Skip to content

Conversation

@CathyL0
Copy link
Contributor

@CathyL0 CathyL0 commented Dec 25, 2025

Summary by CodeRabbit

  • New Features
    • Support for custom BitHuman API endpoints with flexible JSON or multipart/form-data request handling
    • Automatic detection of default vs custom endpoints to choose payload format
    • Optional async operation mode configurable via API URL query parameter
    • Enhanced avatar inputs: strict base64 validation, HTTP(S) URL handling, file upload support, and avatar ID propagation
    • Improved auth for custom endpoints (Bearer) and unified retry/error handling for requests

✏️ Tip: You can customize this high-level summary in your review settings.

@CathyL0 CathyL0 marked this pull request as ready for review January 16, 2026 12:16
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

Routes avatar provisioning between the default BitHuman JSON API and custom endpoints (multipart/form-data). Adds strict base64 validation, async query parsing, unified retry sender, avatar image/ID handling, and conditional auth/header logic, with control flow selection in _start_cloud_agent().

Changes

Cohort / File(s) Summary
BitHuman Avatar Logic
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Added _is_valid_base64, _is_default_api_url, _parse_async_parameter_from_url, _send_json_request, _send_formdata_request, and _send_request_with_retry. Refactored _start_cloud_agent() to choose JSON vs FormData flows, implemented base64 validation/serialization, multipart/form-data construction (file/bytes/URL), unified retry/backoff, and conditional auth header selection (api-secret vs Bearer). Areas to review: base64 edge cases, async query parsing, multipart file handling, retry/backoff correctness, and error propagation.

Sequence Diagram

sequenceDiagram
    participant Avatar as Avatar Service
    participant Detector as Endpoint Detector
    participant JSONPath as JSON Request Path
    participant FormDataPath as FormData Request Path
    participant HTTP as HTTP Client

    Avatar->>Detector: _is_default_api_url()
    alt default API
        Detector-->>Avatar: true
        Avatar->>JSONPath: _send_json_request(livekit_url, token, room)
        JSONPath->>JSONPath: serialize image to base64\nbuild JSON payload\nset headers (Content-Type, api-secret)
        JSONPath->>HTTP: _send_request_with_retry(headers, json_data)
    else custom API
        Detector-->>Avatar: false
        Avatar->>FormDataPath: _parse_async_parameter_from_url()
        Avatar->>FormDataPath: _send_formdata_request(livekit_url, token, room, async_mode)
        FormDataPath->>FormDataPath: build multipart/form-data\nattach file/bytes or URL\nset Authorization: Bearer
        FormDataPath->>HTTP: _send_request_with_retry(headers, form_data)
    end

    HTTP->>HTTP: perform retries/backoff
    alt success
        HTTP-->>Avatar: response
    else failure
        HTTP-->>Avatar: raise APIConnectionError
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I padded through URLs and bytes so spry,
I checked base64 moons in the midnight sky,
I split JSON paths from form-data trails,
Retried with patience when the network ails,
Avatars hop forth — a rabbit's tiny rhyme!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding support for customizable BitHuman GPU avatar endpoint handling, including new methods for endpoint detection, different request formats (JSON vs form-data), and endpoint-specific authentication.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 398-402: Remove trailing whitespace on the reported blank lines in
the avatar.py method that contains the "try:" block using
urlparse(self._api_url); specifically strip trailing spaces from the empty lines
around the try/except and subsequent blocks (the blank lines after the try line,
after parse_qs(parsed.query), and the other blank lines reported) so the file no
longer triggers Ruff W293; run a quick trim of trailing whitespace (or run
ruff/autopep8) and commit the cleaned file.
- Around line 378-386: The headers dict currently becomes dict[str, str | None]
because self._api_secret can be None; before constructing headers in the method
that calls self._send_request_with_retry, narrow self._api_secret to a str by
asserting or casting it to str (e.g., api_secret: str = cast(str,
self._api_secret) or check and raise if None), then use that local api_secret
when building headers = {"Content-Type": "application/json", "api-secret":
api_secret} so headers is typed as dict[str, str].
- Around line 326-337: The _is_default_api_url function currently uses substring
matching which can misclassify custom URLs; change it to parse self._api_url
(use urllib.parse.urlparse) and inspect the parsed.hostname, then return True
only if the hostname equals one of the default domains ("auth.api.bithuman.ai",
"api.bithuman.ai") or is a direct subdomain (e.g., hostname.endswith("." +
domain)) to avoid matching path/query fragments; keep handling when
self._api_url is None.
- Around line 313-321: The custom endpoint branch allows api_token auth via
_send_formdata_request/_is_default_api_url, but _start_cloud_agent (and __init__
cloud-mode validation) still unconditionally requires api_secret; move the
api_secret requirement out of the common path and enforce it only for the
JSON/default API branch so custom endpoints can use api_token-only setups.
Concretely: update _start_cloud_agent to check _is_default_api_url() and only
assert or raise when api_secret is missing for default/JSON flows, and relax the
cloud-mode validation in __init__ to allow custom endpoints without api_secret
(while keeping any existing checks for api_token presence); ensure references to
_parse_async_parameter_from_url remain unchanged.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8086121 and ca532f3.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
  • _ensure_http_session (61-65)
🪛 GitHub Actions: CI
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py

[error] 384-384: mypy: Argument "headers" to "_send_request_with_retry" of "AvatarSession" has incompatible type "dict[str, str | None]"; expected "dict[str, str]"

🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py

[failure] 437-437: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:437:1: W293 Blank line contains whitespace


[failure] 412-412: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:412:1: W293 Blank line contains whitespace


[failure] 402-402: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:402:1: W293 Blank line contains whitespace


[failure] 398-398: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:398:1: W293 Blank line contains whitespace

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

148-154: Cloud mode validation still requires api_secret unconditionally.

The _send_formdata_request method (line 505) allows api_token as an alternative to api_secret for custom endpoints, but __init__ validation at line 151-152 blocks cloud mode without api_secret. Users with custom endpoints who only have api_token will fail initialization.

🐛 Proposed fix
         elif self._mode == "cloud":
             if not utils.is_given(avatar_image) and not utils.is_given(avatar_id):
                 raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode")
-            if self._api_secret is None:
-                raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode")
+            # For custom endpoints, either api_token or api_secret is acceptable
+            # For default endpoints, api_secret is required (validated later in _send_json_request)
+            if self._api_secret is None and self._api_token is None:
+                raise BitHumanException(
+                    "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode"
+                )
             if self._api_url is None:
-                raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")
+                raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")

Note: Also fixed the grammar ("are" → "is") in the error message.

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 336-342: Replace the hostname extraction that uses parsed.netloc
with parsed.hostname so ports won’t break domain comparisons: in the method that
parses self._api_url (the block using parsed = urlparse(self._api_url) and
hostname = parsed.netloc or parsed.hostname), change it to use parsed.hostname
directly, keep the existing None check (if hostname is None: return False), and
then compare hostname against default_domains (["auth.api.bithuman.ai",
"api.bithuman.ai"]) as before.
🧹 Nitpick comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)

320-320: Line exceeds 100 characters.

Per coding guidelines, the maximum line length is 100 characters. This line should be wrapped.

♻️ Proposed fix
-            await self._send_formdata_request(livekit_url, livekit_token, room_name, async_mode=async_mode)
+            await self._send_formdata_request(
+                livekit_url, livekit_token, room_name, async_mode=async_mode
+            )

378-381: Unreachable code branch for bytes type.

The __init__ method (lines 156-165) only sets _avatar_image to Image.Image or str, never bytes. This branch is dead code.

Consider removing this branch or updating __init__ to support bytes input if that's intended functionality.

♻️ Proposed fix (remove dead code)
         if isinstance(self._avatar_image, Image.Image):
             img_byte_arr = io.BytesIO()
             self._avatar_image.save(img_byte_arr, format="JPEG", quality=95)
             img_byte_arr.seek(0)
             import base64

             json_data["image"] = base64.b64encode(img_byte_arr.getvalue()).decode("utf-8")
-        elif isinstance(self._avatar_image, bytes):
-            import base64
-
-            json_data["image"] = base64.b64encode(self._avatar_image).decode("utf-8")
         elif isinstance(self._avatar_image, str):
             json_data["image"] = self._avatar_image

468-476: Unreachable code branch for bytes type (same as _send_json_request).

The __init__ method never sets _avatar_image to bytes, making this branch dead code.

♻️ Proposed fix (remove dead code)
         if isinstance(self._avatar_image, Image.Image):
             # Convert PIL Image to bytes and upload as file
             img_byte_arr = io.BytesIO()
             self._avatar_image.save(img_byte_arr, format="JPEG", quality=95)
             img_byte_arr.seek(0)
             form_data.add_field(
                 "avatar_image",
                 img_byte_arr,
                 filename="avatar.jpg",
                 content_type="image/jpeg",
             )
-        elif isinstance(self._avatar_image, bytes):
-            # Upload raw bytes as file
-            img_byte_arr = io.BytesIO(self._avatar_image)
-            form_data.add_field(
-                "avatar_image",
-                img_byte_arr,
-                filename="avatar.jpg",
-                content_type="image/jpeg",
-            )
         elif isinstance(self._avatar_image, str):

494-496: Silent fallback to URL on base64 decode failure may mask errors.

If the string is intended to be base64 but is malformed, silently treating it as a URL will cause a confusing server-side failure. Consider logging a warning or validating the string format before attempting decode.

♻️ Proposed fix
                 except Exception:
-                    # If decode fails, treat as URL
-                    form_data.add_field("avatar_image_url", self._avatar_image)
+                    # If decode fails, log warning and treat as URL
+                    logger.warning(
+                        "Failed to decode avatar_image as base64, treating as URL",
+                        extra={"avatar_image": self._avatar_image[:50] + "..." if len(self._avatar_image) > 50 else self._avatar_image},
+                    )
+                    form_data.add_field("avatar_image_url", self._avatar_image)

543-547: Passing both json and data to aiohttp.post() is ambiguous.

The docstring states json_data and form_data are mutually exclusive, but both are always passed to post(). While aiohttp prioritizes json when both are provided, this implicit behavior is fragile. Consider adding a runtime check or only passing the non-None parameter.

♻️ Proposed fix
+        if json_data is not None and form_data is not None:
+            raise ValueError("json_data and form_data are mutually exclusive")
+
         for i in range(self._conn_options.max_retry):
             try:
-                async with self._ensure_http_session().post(
-                    self._api_url,
-                    headers=headers,
-                    json=json_data,
-                    data=form_data,
-                    timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout),
-                ) as response:
+                kwargs: dict[str, Any] = {
+                    "headers": headers,
+                    "timeout": aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout),
+                }
+                if json_data is not None:
+                    kwargs["json"] = json_data
+                elif form_data is not None:
+                    kwargs["data"] = form_data
+
+                async with self._ensure_http_session().post(
+                    self._api_url,
+                    **kwargs,
+                ) as response:

Note: You'll need to add from typing import Any if not already imported.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a009b0 and 04434e1.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: type-check (3.13)
  • GitHub Check: type-check (3.9)
  • GitHub Check: unit-tests
🔇 Additional comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

402-426: LGTM!

The method correctly parses the async query parameter with appropriate fallback behavior. The return type annotation and docstring follow the project conventions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 476-495: The code currently treats any non-URL string as base64
and decodes it, which can produce corrupted uploads; update the logic in the
branch handling self._avatar_image (the string path that sets "avatar_image_url"
vs "avatar_image" in form_data) to first validate that the string is valid
base64 before decoding (e.g., check with a base64 regex or use
base64.b64decode(..., validate=True) and ensure proper padding/length), only
then decode to bytes and create the io.BytesIO to add_field("avatar_image"); if
validation fails, fall back to adding as "avatar_image_url" and keep the
existing exception handling for safety.
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

521-565: Add a guard to enforce payload mutual exclusivity.
Helps catch accidental misuse early.

♻️ Proposed refactor
     async def _send_request_with_retry(
         self,
         headers: dict[str, str],
         json_data: dict | None = None,
         form_data: aiohttp.FormData | None = None,
     ) -> None:
@@
-        for i in range(self._conn_options.max_retry):
+        if (json_data is None) == (form_data is None):
+            raise ValueError("Exactly one of json_data or form_data must be provided")
+
+        for i in range(self._conn_options.max_retry):
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04434e1 and 889a8bf.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (6)
livekit-agents/livekit/agents/utils/misc.py (1)
  • is_given (25-26)
livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
  • _ensure_http_session (61-65)
livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/avatar.py (1)
  • _ensure_http_session (63-67)
livekit-plugins/livekit-plugins-hedra/livekit/plugins/hedra/avatar.py (1)
  • _ensure_http_session (69-73)
livekit-plugins/livekit-plugins-simli/livekit/plugins/simli/avatar.py (1)
  • _ensure_http_session (79-83)
livekit-plugins/livekit-plugins-tavus/livekit/plugins/tavus/avatar.py (1)
  • _ensure_http_session (57-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: unit-tests
  • GitHub Check: type-check (3.9)
  • GitHub Check: type-check (3.13)
🔇 Additional comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)

9-9: Import update looks good.
Required for the new URL parsing helpers.


312-325: Clear branching between default and custom endpoints.
Flow is easy to follow and keeps responsibilities separated.


348-399: JSON request path looks solid.
Base64 handling and headers are consistent with the default API flow.


401-425: Async query parsing is clear and safe.
Graceful fallback on parse errors is appropriate here.


327-347: Normalize hostname to avoid case-sensitive misclassification.
If the URL includes uppercase letters, the current comparison can route to the wrong flow.

🔧 Proposed fix
         try:
             parsed = urlparse(self._api_url)
-            hostname = parsed.hostname
+            hostname = parsed.hostname
             if hostname is None:
                 return False
-            default_domains = ["auth.api.bithuman.ai", "api.bithuman.ai"]
-            return hostname in default_domains
+            default_domains = {"auth.api.bithuman.ai", "api.bithuman.ai"}
+            return hostname.lower() in default_domains

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 516-538: The __init__ currently rejects avatar_image values that
are neither file paths nor URLs, preventing valid base64 strings from reaching
the branch that handles them; update the avatar_image validation inside __init__
to also accept strings where _is_valid_base64(self._avatar_image) is True (in
addition to existing path/URL checks) so base64 inputs are stored and later
processed by the block referencing _avatar_image and _is_valid_base64; ensure
the check uses the same _is_valid_base64 helper and does not treat valid base64
as an invalid type that raises an exception.
- Around line 65-82: The function _is_valid_base64 contains blank lines with
trailing whitespace causing Ruff W293; open the _is_valid_base64 implementation
and remove trailing spaces from the blank lines (or run ruff/autoformat) so no
lines in that function end with extraneous whitespace, leaving the logic
(s_clean trimming, empty check, length % 4 check, and base64_pattern match)
unchanged.
♻️ Duplicate comments (2)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (2)

352-365: Custom endpoints are still blocked by cloud-mode api_secret validation.

This branch supports api_token auth for custom endpoints, but __init__ still raises when api_secret is missing, which prevents token-only configurations from reaching this path. Consider gating the api_secret requirement to default endpoints (or requiring either token or secret for custom endpoints).

🔧 Suggested fix (in __init__ cloud-mode validation)
         elif self._mode == "cloud":
             if not utils.is_given(avatar_image) and not utils.is_given(avatar_id):
                 raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode")
-            if self._api_secret is None:
-                raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode")
+            if self._is_default_api_url():
+                if self._api_secret is None:
+                    raise BitHumanException("BITHUMAN_API_SECRET is required for default API")
+            elif self._api_secret is None and self._api_token is None:
+                raise BitHumanException(
+                    "BITHUMAN_API_TOKEN or BITHUMAN_API_SECRET is required for custom endpoints"
+                )
             if self._api_url is None:
                 raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")

428-433: Narrow api_secret to str before building headers.

The assert only excludes None; strict typing can still see a non-str sentinel and produce dict[str, str | NotGiven]. This can break mypy strict and send invalid headers at runtime.

✅ Proposed fix
-        assert self._api_secret is not None, "api_secret is required for default API"
-
-        headers = {
-            "Content-Type": "application/json",
-            "api-secret": self._api_secret,
-        }
+        api_secret = self._api_secret
+        if not isinstance(api_secret, str):
+            raise BitHumanException("BITHUMAN_API_SECRET is required for default API")
+        headers = {
+            "Content-Type": "application/json",
+            "api-secret": api_secret,
+        }

To verify the sentinel’s behavior, inspect NotGiven/NOT_GIVEN definitions and truthiness:

#!/bin/bash
rg -n -C3 "class NotGiven|NOT_GIVEN"
rg -n -C3 "def is_given"
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 889a8bf and f80aaee.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
livekit-agents/livekit/agents/utils/misc.py (1)
  • is_given (25-26)
🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py

[failure] 82-82: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:82:1: W293 Blank line contains whitespace


[failure] 77-77: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:77:1: W293 Blank line contains whitespace


[failure] 72-72: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:72:1: W293 Blank line contains whitespace


[failure] 68-68: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:68:1: W293 Blank line contains whitespace

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: type-check (3.9)
  • GitHub Check: unit-tests
🔇 Additional comments (3)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (3)

367-387: Default endpoint detection via hostname looks good.

The hostname-based check is clear and reduces false positives.


441-465: Async flag parsing looks good.

Graceful handling of missing/invalid query parameters is appropriate here.


564-608: Unified retry sender looks good.

Cleanly consolidates JSON and form-data flows with consistent retry behavior.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (2)

199-205: Base64 string inputs are rejected during initialization, but supported later.

The _send_formdata_request method (lines 522-537) handles base64 strings via _is_valid_base64, but this code path rejects any string that isn't a file path or URL. Base64 avatar images will fail at init before reaching the formdata handler.

🐛 Suggested fix
         elif isinstance(avatar_image, str):
             if os.path.exists(avatar_image):
                 self._avatar_image = Image.open(avatar_image)
             elif avatar_image.startswith("http"):
                 self._avatar_image = avatar_image
+            elif _is_valid_base64(avatar_image):
+                self._avatar_image = avatar_image
             else:
                 raise BitHumanException(f"Invalid avatar image: {avatar_image}")

188-194: Cloud mode validation blocks api_token-only setups for custom endpoints.

The validation at line 191-192 unconditionally requires api_secret for cloud mode. However, _send_formdata_request (lines 549-554) supports api_token as an alternative for custom endpoints. This blocks legitimate use cases where custom endpoints use only api_token.

Also, minor grammar: "are required" → "is required".

🐛 Suggested fix (relax validation for custom endpoints)
         elif self._mode == "cloud":
             if not utils.is_given(avatar_image) and not utils.is_given(avatar_id):
                 raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode")
-            if self._api_secret is None:
-                raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode")
+            # Custom endpoints can use api_token, default API requires api_secret
+            is_default_url = self._api_url is None or any(
+                domain in self._api_url for domain in ["auth.api.bithuman.ai", "api.bithuman.ai"]
+            )
+            if is_default_url and self._api_secret is None:
+                raise BitHumanException("BITHUMAN_API_SECRET is required for default BitHuman API")
+            elif not is_default_url and self._api_secret is None and self._api_token is None:
+                raise BitHumanException(
+                    "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for custom endpoints"
+                )
             if self._api_url is None:
-                raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")
+                raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 420-423: The bytes branch under the serialization logic is dead
because self._avatar_image is typed as Image.Image | str | None and no code sets
bytes; remove the unreachable elif block (the "elif
isinstance(self._avatar_image, bytes):" branch including its base64 import and
assignment to json_data["image"]) from the method that builds json_data so only
the Image.Image and str branches remain; if bytes input should be supported
instead, update the _avatar_image type and any setters to accept bytes and add a
test, otherwise delete this branch and any now-unused imports.
- Around line 538-540: The fallback silently treating non-URL, non-base64
strings as a URL is unsafe; in the method handling avatar upload (look for the
code using self._avatar_image and the form_data.add_field("avatar_image_url",
...)), replace that else branch with explicit validation: if self._avatar_image
does not start with "http://" or "https://" and is not valid base64, raise a
ValueError (or a specific InvalidAvatarError) instead of adding it to form_data
as "avatar_image_url"; keep the existing branches that add "avatar_image" for
base64 data and "avatar_image_url" for valid URLs unchanged.
♻️ Duplicate comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

509-517: Same unreachable code - bytes type check.

This branch has the same issue as _send_json_request: _avatar_image cannot be bytes per its type annotation.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f80aaee and 29d2a2a.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (6)
livekit-agents/livekit/agents/utils/misc.py (1)
  • is_given (25-26)
livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
  • _ensure_http_session (61-65)
livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/avatar.py (1)
  • _ensure_http_session (63-67)
livekit-plugins/livekit-plugins-hedra/livekit/plugins/hedra/avatar.py (1)
  • _ensure_http_session (69-73)
livekit-plugins/livekit-plugins-simli/livekit/plugins/simli/avatar.py (1)
  • _ensure_http_session (79-83)
livekit-plugins/livekit-plugins-tavus/livekit/plugins/tavus/avatar.py (1)
  • _ensure_http_session (57-61)
🔇 Additional comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)

52-90: LGTM - Base64 validation logic is sound.

The validation covers the key checks: non-empty, proper length, character pattern, and successful decoding. The combination of regex pre-check and decode attempt provides good protection against misclassifying non-base64 strings.


352-367: LGTM - Endpoint routing logic is clean.

The routing correctly branches between custom (FormData) and default (JSON) endpoints, with api_secret requirement appropriately scoped to the default API path.


369-388: LGTM - Hostname-based URL detection is correct.

Using parsed.hostname avoids the port-matching issue with netloc, and the fallback to substring matching provides resilience.


443-467: LGTM - Async parameter parsing is straightforward.

The method correctly extracts the async query parameter with appropriate fallback behavior.


566-610: LGTM - Unified retry mechanism is well-structured.

The method cleanly abstracts retry logic for both JSON and FormData payloads with appropriate error handling and logging.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 520-524: Catch the original exception into a variable and use
exception chaining when re-raising BitHumanException so the original traceback
is preserved; change the handler to `except Exception as err:` and raise
`BitHumanException(...) from err` (the code references `self._avatar_image` and
raises `BitHumanException`) so the original error context is attached to the new
exception.
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

63-63: Consider moving re import to module level.

The re module is imported inside the function on each call. For consistency with other imports (like base64 at line 4), consider moving it to the module-level imports.

♻️ Suggested change

At the top of the file (around line 10):

import re

Then remove line 63 from the function.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29d2a2a and f08b9ba.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py

[failure] 522-524: Ruff (B904)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:522:21: B904 Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: unit-tests
  • GitHub Check: type-check (3.13)
🔇 Additional comments (7)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (7)

4-10: LGTM!

The imports are appropriate for the new base64 validation and URL parsing functionality.


188-194: Cloud mode validation may block api_token-only setups for custom endpoints.

The _send_formdata_request method allows either api_token or api_secret (line 539: auth_token = self._api_token or self._api_secret), but the __init__ validation unconditionally requires api_secret for cloud mode. Users wanting to use custom endpoints with only api_token will be blocked at initialization.

Also, minor grammar fix: "are required" → "is required".

🐛 Suggested fix
         elif self._mode == "cloud":
             if not utils.is_given(avatar_image) and not utils.is_given(avatar_id):
                 raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode")
-            if self._api_secret is None:
-                raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode")
+            if self._api_secret is None and self._api_token is None:
+                raise BitHumanException(
+                    "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode"
+                )
             if self._api_url is None:
-                raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")
+                raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")

Please verify this is the intended behavior for custom endpoint authentication.


199-207: LGTM!

The avatar image handling now correctly accepts file paths, HTTP(S) URLs, and valid base64 strings with proper validation.


354-391: LGTM!

Good separation of concerns between custom and default endpoints. The api_secret assertion is now correctly scoped to the default API branch, and _is_default_api_url properly uses hostname parsing.


392-437: LGTM!

The JSON request handling is well-structured with proper avatar image serialization and the api_secret assertion ensures type safety for the headers dictionary.


439-463: LGTM!

Clean implementation for parsing the async parameter from URL query strings with proper error handling.


556-600: LGTM!

Well-designed unified retry mechanism that handles both JSON and FormData payloads with proper error logging and retry intervals.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

188-194: Cloud-mode validation blocks api_token-only setups for custom endpoints.

Lines 191-192 unconditionally require api_secret for all cloud mode, but _send_formdata_request (lines 539-544) accepts either api_token or api_secret. Users with custom endpoints who only have api_token will fail at initialization before reaching the custom endpoint code path.

Additionally, there's a grammar issue: "are required" should be "is required".

🐛 Proposed fix
         elif self._mode == "cloud":
             if not utils.is_given(avatar_image) and not utils.is_given(avatar_id):
                 raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode")
-            if self._api_secret is None:
-                raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode")
+            # Custom endpoints can use api_token, default API requires api_secret
+            # Full validation deferred to request methods
+            if self._api_secret is None and self._api_token is None:
+                raise BitHumanException(
+                    "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode"
+                )
             if self._api_url is None:
-                raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")
+                raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)

556-561: Consider more precise type annotation for json_data.

The dict type is generic. For stricter mypy compliance, consider using dict[str, Any].

♻️ Suggested improvement
     async def _send_request_with_retry(
         self,
         headers: dict[str, str],
-        json_data: dict | None = None,
+        json_data: dict[str, Any] | None = None,
         form_data: aiohttp.FormData | None = None,
     ) -> None:

You'll need to add Any to the imports:

-from typing import TYPE_CHECKING, Literal
+from typing import TYPE_CHECKING, Any, Literal
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f08b9ba and f277761.

📒 Files selected for processing (1)
  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues

Applied to files:

  • livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: type-check (3.13)
  • GitHub Check: type-check (3.9)
  • GitHub Check: unit-tests
🔇 Additional comments (7)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (7)

53-90: LGTM!

The strict base64 validation logic is well-implemented with proper length checks, regex validation, and actual decode verification. Good use of Google-style docstring.


199-207: LGTM!

Good validation flow: file path → URL → base64 → error. This properly accepts all valid avatar image formats while rejecting invalid inputs.


349-369: LGTM!

Good refactoring to separate custom endpoint (form-data) and default API (JSON) paths. The api_secret requirement is correctly enforced only for the default API branch.


371-391: LGTM!

Good use of parsed.hostname instead of netloc to handle URLs with ports. The exception fallback provides reasonable safety.


392-437: LGTM!

Well-structured JSON request method. The assertion at line 426 ensures type narrowing for api_secret, addressing the previous mypy issue.


439-463: LGTM!

Clean implementation for optional async parameter parsing. Silent exception handling is appropriate here since this is an optional enhancement.


465-554: LGTM!

Well-implemented form-data request method with proper exception chaining (from err), strict validation for invalid avatar strings, and flexible auth token handling.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@longcw longcw merged commit 7485986 into livekit:main Jan 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants